/*
This file is part of jasima, the Java simulator for manufacturing and logistics.
 
Copyright 2010-2022 jasima contributors (see license.txt)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package jasima.core.util;

import java.util.HashSet;
import java.util.Set;

import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.javabean.JavaBeanConverter;
import com.thoughtworks.xstream.converters.javabean.JavaBeanProvider;
import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;

/**
 * Converts beans serialized using the jasima GUI, which uses a slightly
 * modified XML format. This converter can read input generated by plain XStream
 * as well as jasima_gui's <code>PermissiveBeanConverter</code>.
 * 
 * @author Robin Kreis
 */
@SuppressWarnings("deprecation")
public class JasimaBeanConverter extends JavaBeanConverter {
	protected static final String NULL_ATTRIBUTE_NAME = "is-null";
	protected static final String NULL_ATTRIBUTE_VALUE = "yes";
	protected boolean saveExtendedInfo;

	public JasimaBeanConverter(Mapper mapper, boolean saveExtendedInfo) {
		super(mapper);
		this.saveExtendedInfo = saveExtendedInfo;
	}

	public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
		if (!saveExtendedInfo) {
			// save plain XStream
			super.marshal(source, writer, context);
			return;
		}

		final String classAttributeName = mapper.aliasForSystemAttribute("class");
		beanProvider.visitSerializableProperties(source, new JavaBeanProvider.Visitor() {
			@SuppressWarnings("rawtypes")
			public boolean shouldVisit(String name, Class definedIn) {
				return mapper.shouldSerializeMember(definedIn, name);
			}

			@SuppressWarnings("rawtypes")
			public void visit(String propertyName, Class fieldType, Class definedIn, Object newObj) {
				if (newObj == null) {
					writer.startNode(propertyName);
					writer.addAttribute(classAttributeName, "null");
					writer.endNode();
				} else {
					Class<?> actualType = newObj.getClass();
					String serializedMember = mapper.serializedMember(source.getClass(), propertyName);
					ExtendedHierarchicalStreamWriterHelper.startNode(writer, serializedMember, actualType);
					writer.addAttribute(classAttributeName, mapper.serializedClass(actualType));
					context.convertAnother(newObj);

					writer.endNode();
				}
			}
		});
	}

	public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) {
		final Object result = instantiate(context);

		final Set<String> seenProperties = new HashSet<>();
		Class<?> resultType = result.getClass();
		while (reader.hasMoreChildren()) {
			reader.moveDown();

			String propertyName = mapper.realMember(resultType, reader.getNodeName());

			if (mapper.shouldSerializeMember(resultType, propertyName)) {
				if (!seenProperties.add(propertyName))
					throw new DuplicatePropertyException(propertyName);

				Object value;

				if (NULL_ATTRIBUTE_VALUE.equals(reader.getAttribute(NULL_ATTRIBUTE_NAME))) {
					value = null;
				} else {
					value = context.convertAnother(result, determineType(reader, result, propertyName));
				}

				beanProvider.writeProperty(result, propertyName, value);
			}
			reader.moveUp();
		}

		return result;
	}

	protected Object instantiate(UnmarshallingContext context) {
		Object result = context.currentObject();
		if (result == null) {
			result = beanProvider.newInstance(context.getRequiredType());
		}
		return result;
	}

	protected Class<?> determineType(HierarchicalStreamReader reader, Object result, String fieldName) {
		final String classAttributeName = mapper.aliasForSystemAttribute("class");
		String classAttribute = classAttributeName == null ? null : reader.getAttribute(classAttributeName);
		if (classAttribute != null) {
			return mapper.realClass(classAttribute);
		} else {
			// only happens with the plain XStream format
			return mapper.defaultImplementationOf(beanProvider.getPropertyType(result, fieldName));
		}
	}
}
